"""
Подробный пост про этот пример: https://amgold.ru/blog/

Упрощенная задача: нужно по полученным с фронта данным – названию ресурса API (допустим, их несколько
десятков), и специфичным для каждого ресурса параметрам (произвольный набор для каждого) сделать
запрос к соответствующему ресурсу API. 

В реальности кроме resource_name нужно добавить еще поле `method` для обращения к различным методам
ресурса, и создать соответствующие подклассы, например NotePost, NoteGetById, NotePatch и т.п.,
прописав в них только специфичные параметры и логику - всё остальное сделает базовый класс.

Reference: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions
"""

from typing import ClassVar, Literal

from pydantic import BaseModel, Field, ValidationError

# Предположим, что в списке - варианты данных, которые мы можем получить с фронта:
# ресурс API, к которому надо обратиться, и специфичные параметры для запроса.
data = [
    {"resource_name": "user", "username": "hazadus"},
    {"resource_name": "client", "phone": "(812) 355-66-55"},
    {"resource_name": "note", "text": "Buy the milk"},
    {"resource_name": "note"},  # Должно вызвать ошибку - нет поля text
    {"resource_name": "unknown"},  # Должно вызвать ошибку - нет такого ресурса
]


class BaseResource(BaseModel):
    # В этой модели могут быть поля, общие для всех ресурсов, например host, auth_token, etc.
    # Опустим их для краткости

    http_method: ClassVar[str] = "GET"

    def _get_request_url(self) -> str: ...
    def _get_headers(self) -> dict: ...
    def _get_json(self) -> dict: ...

    def request(self) -> str:
        return f"{self.http_method} {self._get_request_url()} data={self._get_json()}"


class User(BaseResource):
    # Для каждого ресурса укажем его тип в `resource_name`. По этому полю Pydantic будет отличать ресурсы
    resource_name: Literal["user"]

    # Это поле мы планиуем получить из входных данных, оно будет валидироваться
    username: str

    # Константа, которой не должно быть в данных
    # Ref: https://docs.pydantic.dev/2.6/errors/usage_errors/#model-field-missing-annotation
    endpoint: ClassVar[str] = "/users/"
    http_method: ClassVar[str] = "GET"

    # специфичная логика запроса к ресурсу прописывается здесь, общую
    # нужно вынести в базовый класс
    def _get_request_url(self):
        return f"http://localhost{self.endpoint}{self.username}/?order=desc"


class Client(BaseResource):
    resource_name: Literal["client"]

    phone: str

    endpoint: ClassVar[str] = "/clients/"
    http_method: ClassVar[str] = "POST"

    def _get_request_url(self):
        return f"http://localhost{self.endpoint}"

    def _get_json(self) -> dict:
        return {"phone": self.phone}


class Note(BaseResource):
    resource_name: Literal["note"]

    text: str

    endpoint: ClassVar[str] = "/notes/"
    http_method: ClassVar[str] = "POST"

    def _get_request_url(self):
        return f"http://localhost{self.endpoint}"

    def _get_json(self) -> dict:
        return {"text": self.text}


class API(BaseModel):
    # При помощи union перечисляем, какого типа может быть свойство resource
    # и указываем дискриминатор - поле, по которому будет идентифицирован нужный тип
    resource: User | Client | Note = Field(..., discriminator="resource_name")


for json in data:
    try:
        # ✨ Магия происходит здесь:
        api = API(resource=json)
        # `api.resource` будет инстансом нужного класса, в зависимости от `json.resource_name`!
        # Поэтому `api.resource.request()` запустит специфичную для нужного ресурса логику запроса:
        print(api.resource.request())
    except ValidationError as ex:
        print("\n\nWrong resource type or incorrect params:", ex)
